/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain (a) copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sentry.ohos.core;

import static ohos.aafwk.ability.ProcessErrorInfo.NOT_RESPONDING;

import ohos.aafwk.ability.ProcessErrorInfo;
import ohos.app.Context;
import ohos.app.IAbilityManager;
import ohos.hiviewdfx.Debug;
import io.sentry.ILogger;
import io.sentry.SentryLevel;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;

import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

/**
 * A watchdog timer thread that detects when the UI thread has frozen.
 *
 *  @since 2021-06-01
 */
@SuppressWarnings("UnusedReturnValue")
final class ANRWatchDog extends Thread {
    private final boolean isReportInDebug;
    private final ANRListener anrListener;
    private final IHandler uiHandler;
    private final long timeoutIntervalMillis;
    private final @NotNull ILogger logger;
    private final AtomicLong tick = new AtomicLong(0);
    private final AtomicBoolean reported = new AtomicBoolean(false);

    private final @NotNull Context context;

    @SuppressWarnings("UnnecessaryLambda")
    private final Runnable ticker =
        () -> {
            tick.set(0);
            reported.set(false);
        };

    ANRWatchDog(
        long timeoutIntervalMillis,
        boolean isReportInDebug,
        @NotNull ANRListener listener,
        @NotNull ILogger logger,
        final @NotNull Context context) {
        this(timeoutIntervalMillis, isReportInDebug, listener, logger, new MainLooperHandler(), context);
    }

    @TestOnly
    ANRWatchDog(
        long timeoutIntervalMillis,
        boolean isReportInDebug,
        @NotNull ANRListener listener,
        @NotNull ILogger logger,
        @NotNull IHandler uiHandler,
        final @NotNull Context context) {
        super();
        this.isReportInDebug = isReportInDebug;
        this.anrListener = listener;
        this.timeoutIntervalMillis = timeoutIntervalMillis;
        this.logger = logger;
        this.uiHandler = uiHandler;
        this.context = context;
    }

    /**
     * run
     */
    @Override
    public void run() {
        setName("|ANR-WatchDog|");

        long interval = timeoutIntervalMillis;
        while (!isInterrupted()) {
            boolean isNeedPost = tick.get() == 0;
            tick.addAndGet(interval);
            if (isNeedPost) {
                uiHandler.postTask(ticker);
            }

            try {
                Thread.sleep(interval);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                logger.log(SentryLevel.WARNING, "Interrupted: %s", e.getMessage());
                return;
            }

            // If the main thread has not handled ticker, it is blocked. ANR.
            if (tick.get() != 0 && !reported.get()) {
                if (!isReportInDebug && (Debug.isConnectingToDebugger() || Debug.getDebuggerConnectStatus())) {
                    logger.log(
                        SentryLevel.DEBUG,
                        "An ANR was detected but ignored because the debugger is connected.");
                    reported.set(true);
                    continue;
                }

                // we only raise an ANR event if the process is in ANR state.
                // if IAbilityManager is not available, we'll still be able to send ANRs
                IAbilityManager abilityManager = context.getAbilityManager();
                if (abilityManager != null) {
                    List<ProcessErrorInfo> processesErrorInfo = abilityManager.getProcessesErrorInfo();

                    // if list is null, there's no process in ANR state.
                    if (processesErrorInfo == null) {
                        continue;
                    }
                    boolean isAnr = false;
                    for (ProcessErrorInfo item : processesErrorInfo) {
                        if (item.getCondition() == NOT_RESPONDING) {
                            isAnr = true;
                            break;
                        }
                    }
                    if (!isAnr) {
                        continue;
                    }
                }

                logger.log(SentryLevel.INFO, "Raising ANR");
                final String message =
                    "Application Not Responding for at least " + timeoutIntervalMillis + " ms.";

                final ApplicationNotResponding error =
                    new ApplicationNotResponding(message, uiHandler.getThread());
                anrListener.onAppNotResponding(error);
                interval = timeoutIntervalMillis;

                reported.set(true);
            }
        }
    }

    /**
     * interface ANRListener
     *
     * @since 2021-06-01
     */
    public interface ANRListener {
        /**
         * Called when an ANR is detected.
         *
         * @param error The error describing the ANR.
         */
        void onAppNotResponding(@NotNull ApplicationNotResponding error);
    }
}
